﻿using Microsoft.AspNetCore.Mvc;
using PMS.Core.Configuration.CommonEnumConfigs;
using PMS.Core.Utils;
using PMS.Core.Utils.Authentication;
using PMS.Data.DbUtils;
using PMS.Data.Entities.Customers;
using PMS.Data.Entities.Settings;
using PMS.Services.Caching.Extensions;
using PMS.Services.EntityServices.Customers;
using PMS.Services.SharedKernel;
using PMS.Services.Utils;
using PMS.Services.Utils.Events;
using System;
using System.Collections.Generic;
using System.Linq;

namespace PMS.Services.Authentication.Externel
{
    /// <summary>
    /// Represents external authentication service implementation
    /// </summary>
    public partial class ExternalAuthenticationService : IExternalAuthenticationService
    {
        #region Fields

        private readonly CustomerSettings _customerSettings;
        private readonly IWebHelper _webHelper;
        private readonly IWorkContext _workContext;
        private readonly IStoreContext _storeContext;
        private readonly ICustomerService _customerService;
        private readonly IEventDispatcher _eventDispatcher;
        private readonly IAuthenticationProvider _authenticationProvider;
        private readonly IAuthenticationPluginManager _authenticationPluginManager;
        private readonly ICustomerRegistrationService _customerRegistrationService;
        private readonly IRepository<ExternalAuthenticationRecord> _externalAuthenticationRecordRepository;
        #endregion

        #region Ctor

        public ExternalAuthenticationService(CustomerSettings customerSettings,
            IWebHelper webHelper,
            IWorkContext workContext,
            IStoreContext storeContext,
            IEventDispatcher eventDispatcher,
            ICustomerService customerService,
            IAuthenticationProvider authenticationProvider,
            IAuthenticationPluginManager authenticationPluginManager,
            ICustomerRegistrationService customerRegistrationService,
            IRepository<ExternalAuthenticationRecord> externalAuthenticationRecordRepository)
        {
            _customerSettings = customerSettings;
            _webHelper = webHelper;
            _workContext = workContext;
            _storeContext = storeContext;
            _customerService = customerService;
            _eventDispatcher = eventDispatcher;
            _authenticationProvider = authenticationProvider;
            _authenticationPluginManager = authenticationPluginManager;
            _customerRegistrationService = customerRegistrationService;
            _externalAuthenticationRecordRepository = externalAuthenticationRecordRepository;
        }

        #endregion

        #region Utilities

        /// <summary>
        /// Authenticate user with existing associated external account
        /// </summary>
        /// <param name="associatedUser">Associated with passed external authentication parameters user</param>
        /// <param name="currentLoggedInUser">Current logged-in user</param>
        /// <param name="returnUrl">URL to which the user will return after authentication</param>
        /// <returns>Result of an authentication</returns>
        protected virtual IActionResult AuthenticateExistingUser(Customer associatedUser, Customer currentLoggedInUser, string returnUrl)
        {
            //log in guest user
            if (currentLoggedInUser == null)
                return LoginUser(associatedUser, returnUrl);

            //account is already assigned to another user
            if (currentLoggedInUser.Id != associatedUser.Id)
                return ErrorAuthentication(new[] { $"当前已登录用户已绑定其他授权账户,如非本人操作,请登录系统解绑第三方授权账户" }, returnUrl);

            //or the user try to log in as himself. bit weird
            return SuccessfulAuthentication(returnUrl);
        }

        /// <summary>
        /// Authenticate current user and associate new external account with user
        /// </summary>
        /// <param name="currentLoggedInUser">Current logged-in user</param>
        /// <param name="parameters">Authentication parameters received from external authentication method</param>
        /// <param name="returnUrl">URL to which the user will return after authentication</param>
        /// <returns>Result of an authentication</returns>
        protected virtual IActionResult AuthenticateNewUser(Customer currentLoggedInUser, ExternalAuthenticationParameters parameters, string returnUrl)
        {
            //associate external account with logged-in user
            if (currentLoggedInUser != null)
            {
                AssociateExternalAccountWithUser(currentLoggedInUser, parameters);
                return new RedirectToRouteResult("BindExternalAccountWithUserSuccess", new { returnUrl = returnUrl, customerGuid = currentLoggedInUser.CustomerGuid.ToString() });
            }

            //or try to register new user
            if (_customerSettings.UserRegistrationType != UserRegistrationType.Disabled)
                return RegisterNewUser(parameters, returnUrl);

            //registration is disabled
            //return ErrorAuthentication(new[] { "Registration is disabled" }, returnUrl);
            return new RedirectToActionResult("RegisterDisallowdView", "Customer", null, false);
        }

        /// <summary>
        /// Register new user
        /// </summary>
        /// <param name="parameters">Authentication parameters received from external authentication method</param>
        /// <param name="returnUrl">URL to which the user will return after authentication</param>
        /// <returns>Result of an authentication</returns>
        protected virtual IActionResult RegisterNewUser(ExternalAuthenticationParameters parameters, string returnUrl)
        {
            Customer registedCustomer = _customerService.GetCustomerByEmail(parameters.Email);
            if (registedCustomer == null)
                registedCustomer = _customerService.GetCustomerByMobile(parameters.Mobile);
            //check whether the specified email has been already registered
            if (registedCustomer != null)
            {
                /***
                * 以下注关联授权账户与本地账户逻辑不通，不允许使用注释代码直接通过绑定未登录用户
                * 只有已登录系统用户可以直接关联并绑定第三方授权登录账户
                * 否则用户可以在登录页直接扫码(或授权)关联其他已注册但未登录账户，存在不安全隐患
                AssociateExternalAccountWithUser(registedCustomer, parameters);
                return SuccessfulAuthentication(returnUrl);
                **/
                var alreadyExistsError = "未登录系统用户禁止绑定系统已注册账户,请先登录系统再关联第三方授权账户";
                return ErrorAuthentication(new[] { alreadyExistsError } );
            }
            else
            {
                IList<string> errorMsgs = new List<string>();
                if (string.IsNullOrEmpty(parameters.Mobile))
                    errorMsgs.Add("无法完成自动关联账户注册：第三方授权登录没有获取到必要的用户信息(手机号)。");
                if (string.IsNullOrEmpty(parameters.Email))
                {
                    if (_customerSettings.UserRegistrationType == UserRegistrationType.MobileOnly)
                        parameters.Email = parameters.Mobile + "@current-store-name.com";
                    else
                        errorMsgs.Add("无法完成自动关联账户注册：第三方授权登录没有获取到必要的用户信息(邮箱号)。");
                }
                if(errorMsgs.Count > 0)
                {
                    errorMsgs.Add("您可以：1、登录系统后绑定授权账户; 2、注册新账户; 3:更换其他第三方授权登录重试;");
                    return ErrorAuthentication(errorMsgs, AuthenticationDefaultConfigs.LoginPath);
                }
            }

            //create registration request
            //已登录系统用户无法到达此处，如果通过_workContext.CurrentCustomer能获取到当前登录用户
            //则说明流程设计有问题
            if (_workContext.CurrentCustomer != null)
                throw new PMSException("Error ExternalAuthentication WorkFlow, LoggedInCustomer not supposed to get this far!!", SystemLogLevel.Fatal);
            registedCustomer = new Customer
            {
                Email = parameters.Email,
                Mobile = parameters.Mobile,
                LoginName = $"customer_{parameters.Mobile}",
                NickName = parameters.Mobile,
                HasShoppingCartItems = false,
                RequireChangingPwd = false,
                FailedLoginAttempts = 0,
                CannotLoginUntilDateUtc = null,
                IsActive = true,
                IsDeleted = false,
                LastActivityTimeUtc = DateTime.UtcNow,
                CreatedOnTimeUtc = DateTime.UtcNow,
                UpdatedOnTimeUtc = DateTime.UtcNow,
                LastLoginTimeUtc = DateTime.UtcNow,
                LastLoginIpAddress = _webHelper.GetCurrentIpAddress(),
                RegisteredInStoreId = _storeContext.CurrentStore.Id
            };
            var registrationRequest = new CustomerRegistrationRequest(registedCustomer,
                CommonHelper.CreateRandomPasswordWithRandomLength(),
                PasswordFormat.Hashed,
                PasswordStrongLevel.Strong,
                true,
                true,
                true);

            //whether registration request has been completed successfully
            var registrationResult = _customerRegistrationService.RegisterCustomer(registrationRequest);
            if (!registrationResult.Success)
                return ErrorAuthentication(registrationResult.Errors);

            //allow to save other customer values by consuming this event
            _eventDispatcher.Dispatch(new CustomerAutoRegisteredByExternalMethodEvent(registedCustomer, parameters));

            //associate external account with registered user
            AssociateExternalAccountWithUser(registedCustomer, parameters);

            return new RedirectToRouteResult("BindExternalAccountWithUserSuccess");
        }

        /// <summary>
        /// Login passed user
        /// </summary>
        /// <param name="user">User to login</param>
        /// <param name="returnUrl">URL to which the user will return after authentication</param>
        /// <returns>Result of an authentication</returns>
        protected virtual IActionResult LoginUser(Customer user, string returnUrl)
        {
            //migrate shopping cart
            //_shoppingCartService.MigrateShoppingCart(_workContext.CurrentCustomer, user, true);

            //authenticate
            _authenticationProvider.AuthServcice.SignIn(user, false);

            //reserved event for further customer login operates
            _eventDispatcher.Dispatch(new CustomerLoggedinEvent(user));

            //activity log
            //_customerActivityService.InsertActivity(user, "PublicStore.Login",
            //    _localizationService.GetResource("ActivityLog.PublicStore.Login"), user);

            return SuccessfulAuthentication(returnUrl);
        }

        /// <summary>
        /// Add errors that occurred during authentication
        /// </summary>
        /// <param name="errors">Collection of errors</param>
        /// <param name="returnUrl">URL to which the user will return after authentication</param>
        /// <returns>Result of an authentication</returns>
        protected virtual IActionResult ErrorAuthentication(IEnumerable<string> errors, string returnurl = null)
        {
            foreach (var error in errors)
                ExternalAuthorizerHelper.AddErrorsToDisplay(error);

            return new RedirectToActionResult("ErrorPage", "Common", !string.IsNullOrEmpty(returnurl) ? new { returnurl } : null);
        }

        /// <summary>
        /// Redirect the user after successful authentication
        /// </summary>
        /// <param name="returnUrl">URL to which the user will return after authentication</param>
        /// <returns>Result of an authentication</returns>
        protected virtual IActionResult SuccessfulAuthentication(string returnUrl)
        {
            //redirect to the return URL if it's specified
            if (!string.IsNullOrEmpty(returnUrl))
                return new RedirectResult(returnUrl);

            return new RedirectToRouteResult("HomePage", null);
        }

        #endregion

        #region Methods

        #region Authentication

        /// <summary>
        /// Authenticate user by passed parameters
        /// 第三方授权绑定本地账户流程图见: ~/PMS.Web/App_Data/第三方授权登录绑定本地账户流程图.iodraw
        /// https://www.iodraw.com/ 打开上传文件查看流程
        /// </summary>
        /// <param name="parameters">External authentication parameters</param>
        /// <param name="returnUrl">URL to which the user will return after authentication</param>
        /// <returns>Result of an authentication</returns>
        public virtual IActionResult Authenticate(ExternalAuthenticationParameters parameters, Customer curLoggedinCustomer, string returnUrl = null)
        {
            if (parameters == null)
                throw new ArgumentNullException(nameof(parameters));

            if (!_authenticationPluginManager.IsPluginActive(parameters.ProviderSystemName, _workContext.CurrentCustomer, _storeContext.CurrentStore.Id))
                return ErrorAuthentication(new[] { "External authentication method cannot be loaded" } );

            //get current logged-in user
            if(curLoggedinCustomer != null && _workContext.CurrentCustomer == null)
            {
                _workContext.CurrentCustomer = curLoggedinCustomer;
            }
            var currentLoggedInUser = _customerService.IsRegistered(_workContext.CurrentCustomer) ? _workContext.CurrentCustomer : null;

            //authenticate associated user if already exists
            var associatedUser = GetUserByExternalAuthenticationParameters(parameters);
            if (associatedUser != null)
                return AuthenticateExistingUser(associatedUser, currentLoggedInUser, returnUrl);

            //or associate and authenticate new user
            return AuthenticateNewUser(currentLoggedInUser, parameters, returnUrl);
        }

        #endregion

        /// <summary>
        /// Associate external account with customer
        /// </summary>
        /// <param name="customer">Customer</param>
        /// <param name="parameters">External authentication parameters</param>
        public virtual void AssociateExternalAccountWithUser(Customer customer, ExternalAuthenticationParameters parameters)
        {
            if (customer == null)
                throw new ArgumentNullException(nameof(customer));

            //有可能授权账户邮箱/手机号与登录账号邮箱/手机号不一致
            //甚至授权账户手机号为非大陆大区手机号(如facebook授权等)
            //故只能使用关联用户Id进行登录
            var externalAuthenticationRecord = new ExternalAuthenticationRecord
            {
                CustomerId = customer.Id,
                Email = string.IsNullOrEmpty(parameters.Email) ? customer.Email : parameters.Email,
                Mobile = string.IsNullOrEmpty(parameters.Mobile) ? customer.Mobile : parameters.Mobile,
                ExternalIdentifier = parameters.ExternalIdentifier,
                ExternalIdentifierName = parameters.ExternalIdentifierName,
                OAuthAccessToken = parameters.AccessToken,
                AccessTokenExpiredSeconds = parameters.AccessTokenExpiredSeconds,
                ProviderSystemName = parameters.ProviderSystemName,
                CreatedOnTimeUtc = DateTime.UtcNow,
                UpdatedOnTimeUtc = DateTime.UtcNow
            };

            _externalAuthenticationRecordRepository.Insert(externalAuthenticationRecord);
        }

        /// <summary>
        /// Get the particular user with specified parameters
        /// </summary>
        /// <param name="parameters">External authentication parameters</param>
        /// <returns>Customer</returns>
        public virtual Customer GetUserByExternalAuthenticationParameters(ExternalAuthenticationParameters parameters)
        {
            if (parameters == null)
                throw new ArgumentNullException(nameof(parameters));

            var associationRecord = _externalAuthenticationRecordRepository.Table.FirstOrDefault(record =>
                record.ExternalIdentifier.Equals(parameters.ExternalIdentifier) && record.ProviderSystemName.Equals(parameters.ProviderSystemName));
            if (associationRecord == null)
                return null;

            return _customerService.GetCustomerById(associationRecord.CustomerId);
        }

        /// <summary>
        /// Get the external authentication records by identifier
        /// </summary>
        /// <param name="externalAuthenticationRecordId">External authentication record identifier</param>
        /// <returns>Result</returns>
        public virtual ExternalAuthenticationRecord GetExternalAuthenticationRecordById(int externalAuthenticationRecordId)
        {
            if (externalAuthenticationRecordId == 0)
                return null;

            return _externalAuthenticationRecordRepository.ToCachedGetById(externalAuthenticationRecordId);
        }

        /// <summary>
        /// Get list of the external authentication records by customer
        /// </summary>
        /// <param name="customer">Customer</param>
        /// <returns>Result</returns>
        public virtual IList<ExternalAuthenticationRecord> GetCustomerExternalAuthenticationRecords(Customer customer)
        {
            if (customer == null)
                throw new ArgumentNullException(nameof(customer));

            var associationRecords = _externalAuthenticationRecordRepository.Table.Where(ear => ear.CustomerId == customer.Id);

            return associationRecords.ToList();
        }

        /// <summary>
        /// Remove the association
        /// </summary>
        /// <param name="parameters">External authentication parameters</param>
        public virtual void RemoveAssociation(ExternalAuthenticationParameters parameters)
        {
            if (parameters == null)
                throw new ArgumentNullException(nameof(parameters));

            var associationRecord = _externalAuthenticationRecordRepository.Table.FirstOrDefault(record =>
                record.ExternalIdentifier.Equals(parameters.ExternalIdentifier) && record.ProviderSystemName.Equals(parameters.ProviderSystemName));

            if (associationRecord != null)
                _externalAuthenticationRecordRepository.Delete(associationRecord);
        }

        /// <summary>
        /// Delete the external authentication record
        /// </summary>
        /// <param name="externalAuthenticationRecord">External authentication record</param>
        public virtual void DeleteExternalAuthenticationRecord(ExternalAuthenticationRecord externalAuthenticationRecord)
        {
            if (externalAuthenticationRecord == null)
                throw new ArgumentNullException(nameof(externalAuthenticationRecord));

            _externalAuthenticationRecordRepository.Delete(externalAuthenticationRecord);
        }

        #endregion
    }
}
